using System;
using ch.ethz.ssh2.crypto.cipher;
using ch.ethz.ssh2.crypto.digest;
using ch.ethz.ssh2.signature;
using ch.ethz.ssh2.util;
using Org.BouncyCastle.Math;
namespace ch.ethz.ssh2.crypto
{

    /// <summary> PEM Support.
    /// 
    /// </summary>
    /// <author>  Christian Plattner
    /// </author>
    /// <version>  2.50, 03/15/10
    /// </version>
    public class PEMDecoder
    {
        private const int PEM_RSA_PRIVATE_KEY = 1;
        private const int PEM_DSA_PRIVATE_KEY = 2;

        private static int hexToInt(char c)
        {
            if ((c >= 'a') && (c <= 'f'))
            {
                return (c - 'a') + 10;
            }

            if ((c >= 'A') && (c <= 'F'))
            {
                return (c - 'A') + 10;
            }

            if ((c >= '0') && (c <= '9'))
            {
                return (c - '0');
            }

            throw new System.ArgumentException("Need hex char");
        }

        private static byte[] hexToByteArray(System.String hex)
        {
            if (hex == null)
                throw new System.ArgumentException("null argument");

            if ((hex.Length % 2) != 0)
                throw new System.ArgumentException("Uneven string length in hex encoding.");

            byte[] decoded = new byte[hex.Length / 2];

            for (int i = 0; i < decoded.Length; i++)
            {
                int hi = hexToInt(hex[i * 2]);
                int lo = hexToInt(hex[(i * 2) + 1]);

                decoded[i] = (byte)(hi * 16 + lo);
            }

            return decoded;
        }

        private static byte[] generateKeyFromPasswordSaltWithMD5(byte[] password, byte[] salt, int keyLen)
        {
            if (salt.Length < 8)
                throw new System.ArgumentException("Salt needs to be at least 8 bytes for key generation.");

            MD5 md5 = new MD5();

            byte[] key = new byte[keyLen];
            byte[] tmp = new byte[md5.DigestLength];

            while (true)
            {
                md5.update(password, 0, password.Length);
                md5.update(salt, 0, 8); // ARGH we only use the first 8 bytes of the salt in this step.
                // This took me two hours until I got AES-xxx running.

                int copy = (keyLen < tmp.Length) ? keyLen : tmp.Length;

                md5.digest(tmp, 0);

                Array.Copy(tmp, 0, key, key.Length - keyLen, copy);

                keyLen -= copy;

                if (keyLen == 0)
                    return key;

                md5.update(tmp, 0, tmp.Length);
            }
        }

        private static byte[] removePadding(byte[] buff, int blockSize)
        {
            /* Removes RFC 1423/PKCS #7 padding */

            int rfc_1423_padding = buff[buff.Length - 1] & 0xff;

            if ((rfc_1423_padding < 1) || (rfc_1423_padding > blockSize))
                throw new System.IO.IOException("Decrypted PEM has wrong padding, did you specify the correct password?");

            for (int i = 2; i <= rfc_1423_padding; i++)
            {
                if (buff[buff.Length - i] != rfc_1423_padding)
                    throw new System.IO.IOException("Decrypted PEM has wrong padding, did you specify the correct password?");
            }

            byte[] tmp = new byte[buff.Length - rfc_1423_padding];
            Array.Copy(buff, 0, tmp, 0, buff.Length - rfc_1423_padding);
            return tmp;
        }

        private static PEMStructure parsePEM(char[] pem)
        {
            PEMStructure ps = new PEMStructure();

            System.String line = null;

            //UPGRADE_ISSUE: Constructor 'java.io.BufferedReader.BufferedReader' was not converted. "ms-help://MS.VSCC.v80/dv_commoner/local/redirect.htm?index='!DefaultContextWindowIndex'&keyword='jlca1000_javaioBufferedReaderBufferedReader_javaioReader'"
            System.IO.StringReader br = new System.IO.StringReader(Convert.ToString(pem));

            System.String endLine = null;

            while (true)
            {
                line = br.ReadLine();

                if (line == null)
                    throw new System.IO.IOException("Invalid PEM structure, '-----BEGIN...' missing");

                line = line.Trim();

                if (line.StartsWith("-----BEGIN DSA PRIVATE KEY-----"))
                {
                    endLine = "-----END DSA PRIVATE KEY-----";
                    ps.pemType = PEM_DSA_PRIVATE_KEY;
                    break;
                }

                if (line.StartsWith("-----BEGIN RSA PRIVATE KEY-----"))
                {
                    endLine = "-----END RSA PRIVATE KEY-----";
                    ps.pemType = PEM_RSA_PRIVATE_KEY;
                    break;
                }
            }

            while (true)
            {
                line = br.ReadLine();

                if (line == null)
                    throw new System.IO.IOException("Invalid PEM structure, " + endLine + " missing");

                line = line.Trim();

                int sem_idx = line.IndexOf(':');

                if (sem_idx == -1)
                    break;

                System.String name = line.Substring(0, (sem_idx + 1) - (0));
                System.String valueStr = line.Substring(sem_idx + 1);

                System.String[] values = valueStr.Split(',');

                for (int i = 0; i < values.Length; i++)
                    values[i] = values[i].Trim();

                // Proc-Type: 4,ENCRYPTED
                // DEK-Info: DES-EDE3-CBC,579B6BE3E5C60483

                if ("Proc-Type:".Equals(name))
                {
                    ps.procType = values;
                    continue;
                }

                if ("DEK-Info:".Equals(name))
                {
                    ps.dekInfo = values;
                    continue;
                }
                /* Ignore line */
            }

            System.Text.StringBuilder keyData = new System.Text.StringBuilder();

            while (true)
            {
                if (line == null)
                    throw new System.IO.IOException("Invalid PEM structure, " + endLine + " missing");

                line = line.Trim();

                if (line.StartsWith(endLine))
                    break;

                keyData.Append(line);

                line = br.ReadLine();
            }

            char[] pem_chars = new char[keyData.Length];
            int i2;
            int j;
            i2 = 0;
            j = 0;
            while (i2 < pem_chars.Length)
            {
                pem_chars[j] = keyData[i2];
                i2++;
                j++;
            }

            ps.data = Base64.decode(pem_chars);

            if (ps.data.Length == 0)
                throw new System.IO.IOException("Invalid PEM structure, no data available");

            return ps;
        }

        private static void decryptPEM(PEMStructure ps, byte[] pw)
        {
            if (ps.dekInfo == null)
                throw new System.IO.IOException("Broken PEM, no mode and salt given, but encryption enabled");

            if (ps.dekInfo.Length != 2)
                throw new System.IO.IOException("Broken PEM, DEK-Info is incomplete!");

            System.String algo = ps.dekInfo[0];
            byte[] salt = hexToByteArray(ps.dekInfo[1]);

            BlockCipher bc = null;

            if (algo.Equals("DES-EDE3-CBC"))
            {
                DESede des3 = new DESede();
                des3.init(false, generateKeyFromPasswordSaltWithMD5(pw, salt, 24));
                bc = new CBCMode(des3, salt, false);
            }
            else if (algo.Equals("DES-CBC"))
            {
                DES des = new DES();
                des.init(false, generateKeyFromPasswordSaltWithMD5(pw, salt, 8));
                bc = new CBCMode(des, salt, false);
            }
            else if (algo.Equals("AES-128-CBC"))
            {
                AES aes = new AES();
                aes.init(false, generateKeyFromPasswordSaltWithMD5(pw, salt, 16));
                bc = new CBCMode(aes, salt, false);
            }
            else if (algo.Equals("AES-192-CBC"))
            {
                AES aes = new AES();
                aes.init(false, generateKeyFromPasswordSaltWithMD5(pw, salt, 24));
                bc = new CBCMode(aes, salt, false);
            }
            else if (algo.Equals("AES-256-CBC"))
            {
                AES aes = new AES();
                aes.init(false, generateKeyFromPasswordSaltWithMD5(pw, salt, 32));
                bc = new CBCMode(aes, salt, false);
            }
            else
            {
                throw new System.IO.IOException("Cannot decrypt PEM structure, unknown cipher " + algo);
            }

            if ((ps.data.Length % bc.BlockSize) != 0)
                throw new System.IO.IOException("Invalid PEM structure, size of encrypted block is not a multiple of " + bc.BlockSize);

            /* Now decrypt the content */

            byte[] dz = new byte[ps.data.Length];

            for (int i = 0; i < ps.data.Length / bc.BlockSize; i++)
            {
                bc.transformBlock(ps.data, i * bc.BlockSize, dz, i * bc.BlockSize);
            }

            /* Now check and remove RFC 1423/PKCS #7 padding */

            dz = removePadding(dz, bc.BlockSize);

            ps.data = dz;
            ps.dekInfo = null;
            ps.procType = null;
        }

        public static bool isPEMEncrypted(PEMStructure ps)
        {
            if (ps.procType == null)
                return false;

            if (ps.procType.Length != 2)
                throw new System.IO.IOException("Unknown Proc-Type field.");

            if ("4".Equals(ps.procType[0]) == false)
                throw new System.IO.IOException("Unknown Proc-Type field (" + ps.procType[0] + ")");

            if ("ENCRYPTED".Equals(ps.procType[1]))
                return true;

            return false;
        }

        public static System.Object decode(char[] pem, System.String password)
        {
            PEMStructure ps = parsePEM(pem);

            if (isPEMEncrypted(ps))
            {
                if (password == null)
                    throw new System.IO.IOException("PEM is encrypted, but no password was specified");

                decryptPEM(ps, StringEncoder.GetBytes(password));
            }

            if (ps.pemType == PEM_DSA_PRIVATE_KEY)
            {
                SimpleDERReader dr = new SimpleDERReader(ps.data);

                byte[] seq = dr.readSequenceAsByteArray();

                if (dr.available() != 0)
                    throw new System.IO.IOException("Padding in DSA PRIVATE KEY DER stream.");

                dr.resetInput(seq);
                BigInteger version = dr.readInt();

                if (version.CompareTo((System.Object)new Decimal(0)) != 0)
                    throw new System.IO.IOException("Wrong version (" + version + ") in DSA PRIVATE KEY DER stream.");

                BigInteger p = dr.readInt();
                BigInteger q = dr.readInt();
                BigInteger g = dr.readInt();
                BigInteger y = dr.readInt();
                BigInteger x = dr.readInt();

                if (dr.available() != 0)
                    throw new System.IO.IOException("Padding in DSA PRIVATE KEY DER stream.");

                return new DSAPrivateKey(p, q, g, y, x);
            }

            if (ps.pemType == PEM_RSA_PRIVATE_KEY)
            {
                SimpleDERReader dr = new SimpleDERReader(ps.data);

                byte[] seq = dr.readSequenceAsByteArray();

                if (dr.available() != 0)
                    throw new System.IO.IOException("Padding in RSA PRIVATE KEY DER stream.");

                dr.resetInput(seq);

                BigInteger version = dr.readInt();

                if ((version.CompareTo((System.Object)new Decimal(0)) != 0) && (version.CompareTo((System.Object)new Decimal(1)) != 0))
                    throw new System.IO.IOException("Wrong version (" + version + ") in RSA PRIVATE KEY DER stream.");

                BigInteger n = dr.readInt();
                BigInteger e = dr.readInt();
                BigInteger d = dr.readInt();

                return new RSAPrivateKey(d, e, n);
            }

            throw new System.IO.IOException("PEM problem: it is of unknown type");
        }
    }
}